﻿namespace MetaProperties.Collections
{
    using System;
    using System.Collections.Generic;
    using System.Collections.Specialized;

    /// <summary>
    /// An observable keyed collection.
    /// </summary>
    /// <typeparam name="KeyT">The type of the key for the elements in the list.</typeparam>
    /// <typeparam name="ItemT">The type of the elements in the list.</typeparam>
    public partial class ObservableKeyedCollection<KeyT, ItemT> : IObservableKeyedCollection<KeyT, ItemT>
    {
        /// <summary>
        /// The keyed list of items.
        /// </summary>
        private readonly Dictionary<KeyT, ItemT> dictionary;
        
        /// <summary>
        /// The ordered list of items.
        /// </summary>
        private readonly ObservableList<ItemT> list;

        /// <summary>
        /// The function for returning a key.
        /// </summary>
        private readonly Func<ItemT, KeyT> getKeyFunction;

        /// <summary>
        /// Initializes a new instance of the <see cref="ObservableKeyedCollection&lt;KeyT, ItemT&gt;"/> class.
        /// </summary>
        /// <param name="getKeyFunction">The get key function.</param>
        /// <param name="comparer">The comparer.</param>
        public ObservableKeyedCollection(Func<ItemT, KeyT> getKeyFunction, IEqualityComparer<KeyT> comparer)
        {
            this.getKeyFunction = getKeyFunction;

            this.dictionary = new Dictionary<KeyT, ItemT>(comparer);
            this.list = new ObservableList<ItemT>();

            this.list.CollectionChanged += this.List_CollectionChanged;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ObservableKeyedCollection&lt;KeyT, ItemT&gt;"/> class.
        /// </summary>
        /// <param name="getKeyFunction">The get key function.</param>
        public ObservableKeyedCollection(Func<ItemT, KeyT> getKeyFunction)
            : this(getKeyFunction, EqualityComparer<KeyT>.Default)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ObservableKeyedCollection&lt;KeyT, ItemT&gt;"/> class.
        /// </summary>
        /// <param name="getKeyFunction">The get key function.</param>
        /// <param name="items">The items.</param>
        public ObservableKeyedCollection(Func<ItemT, KeyT> getKeyFunction, IEnumerable<ItemT> items)
            : this(getKeyFunction)
        {
            this.AddRange(items);
        }

        /// <summary>
        /// Occurs when the items list of the collection has changed, or the collection is reset.
        /// </summary>
        public event NotifyCollectionChangedEventHandler CollectionChanged;

        /// <summary>
        /// Gets the number of elements contained in the <see cref="T:System.Collections.Generic.ICollection`1"/>.
        /// </summary>
        /// <value></value>
        /// <returns>The number of elements contained in the <see cref="T:System.Collections.Generic.ICollection`1"/>.</returns>
        public int Count
        {
            get
            {
                return this.list.Count;
            }
        }

        /// <summary>
        /// Gets a value indicating whether the <see cref="T:System.Collections.Generic.ICollection`1"/> is read-only.
        /// </summary>
        /// <returns>true if the <see cref="T:System.Collections.Generic.ICollection`1"/> is read-only; otherwise, false.</returns>
        public bool IsReadOnly
        {
            get 
            { 
                return false; 
            }
        }

        /// <summary>
        /// Gets or sets the item at the specified index.
        /// </summary>
        /// <param name="index">The zero-based index of the item.</param>
        public ItemT this[int index]
        {
            get
            {
                // This is so that if KeyT is an int, the collection behaves as a normal dictionary.
                if (typeof(KeyT) == typeof(int))
                {
                    return this.Get((KeyT)(object)index);
                }
                else
                {
                    return this.list[index];
                }
            }

            set
            {
                // This is so that if KeyT is an int, the collection behaves as a normal dictionary.
                if (typeof(KeyT) == typeof(int))
                {
                    this.Set((KeyT)(object)index, value);
                }
                else
                {
                    if (index >= 0 && index < this.list.Count)
                    {
                        ItemT item = this.list[index];

                        this.dictionary[this.getKeyFunction(item)] = value;
                        this.list[index] = value;
                    }
                    else
                    {
                        throw new IndexOutOfRangeException();
                    }
                }
            }
        }

        /// <summary>
        /// Gets the item with the specified key.
        /// </summary>
        /// <param name="key">The key of the item.</param>
        public ItemT this[KeyT key]
        {
            get
            {
                return this.Get(key);
            }
        }

        /// <summary>
        /// Returns the items as an array.
        /// </summary>
        /// <returns>The items as an array.</returns>
        public ItemT[] ToArray()
        {
            ItemT[] array = new ItemT[this.list.Count];
            for (int i = 0; i < array.Length; i++)
            {
                array[i] = this.list[i];
            }

            return array;
        }

        /// <summary>
        /// Removes all items from the <see cref="T:System.Collections.Generic.ICollection`1"/>.
        /// </summary>
        /// <exception cref="T:System.NotSupportedException">The <see cref="T:System.Collections.Generic.ICollection`1"/> is read-only. </exception>
        public void Clear()
        {
            this.dictionary.Clear();
            this.list.Clear();
        }

        /// <summary>
        /// Removes the <see cref="T:System.Collections.Generic.IList`1"/> item at the specified index.
        /// </summary>
        /// <param name="index">The zero-based index of the item to remove.</param>
        /// <exception cref="T:System.ArgumentOutOfRangeException">
        ///  <paramref name="index"/> is not a valid index in the <see cref="T:System.Collections.Generic.IList`1"/>.</exception>
        /// <exception cref="T:System.NotSupportedException">The <see cref="T:System.Collections.Generic.IList`1"/> is read-only.</exception>
        public void RemoveAt(int index)
        {
            ItemT item = this.list[index];
            this.dictionary.Remove(this.getKeyFunction(item));
            this.list.RemoveAt(index);
        }

        /// <summary>
        /// Inserts an item to the <see cref="T:System.Collections.Generic.IList`1"/> at the specified index.
        /// </summary>
        /// <param name="index">The zero-based index at which <paramref name="item"/> should be inserted.</param>
        /// <param name="item">The object to insert into the <see cref="T:System.Collections.Generic.IList`1"/>.</param>
        /// <exception cref="T:System.ArgumentOutOfRangeException">
        ///  <paramref name="index"/> is not a valid index in the <see cref="T:System.Collections.Generic.IList`1"/>.</exception>
        /// <exception cref="T:System.NotSupportedException">The <see cref="T:System.Collections.Generic.IList`1"/> is read-only.</exception>
        public void Insert(int index, ItemT item)
        {
            this.dictionary.Add(this.getKeyFunction(item), item);
            this.list.Insert(index, item);
        }

        /// <summary>
        /// Adds an item to the <see cref="T:System.Collections.Generic.ICollection`1"/>.
        /// </summary>
        /// <param name="item">The object to add to the <see cref="T:System.Collections.Generic.ICollection`1"/>.</param>
        /// <exception cref="T:System.NotSupportedException">The <see cref="T:System.Collections.Generic.ICollection`1"/> is read-only.</exception>
        public void Add(ItemT item)
        {
            this.dictionary.Add(this.getKeyFunction(item), item);
            this.list.Add(item);
        }

        /// <summary>
        /// Adds the specified items to the collection.
        /// </summary>
        /// <param name="items">The items to add.</param>
        public void AddRange(IEnumerable<ItemT> items)
        {
            if (items != null)
            {
                foreach (ItemT item in items)
                {
                    this.dictionary.Add(this.getKeyFunction(item), item);
                    this.list.Add(item);
                }
            }
        }

        /// <summary>
        /// Determines whether the collection contains an item with the specified key.
        /// </summary>
        /// <param name="key">The key of the item.</param>
        /// <returns>
        ///  <c>true</c> if an item with the specified key exists; otherwise, <c>false</c>.
        /// </returns>
        public bool Contains(KeyT key)
        {
            return this.dictionary.ContainsKey(key);
        }

        /// <summary>
        /// Gets the index of the item with the speficied key.
        /// </summary>
        /// <param name="key">The key of the item.</param>
        /// <returns>
        /// The index of the item with the speficied key.
        /// </returns>
        public int IndexOf(KeyT key)
        {
            for (int i = 0; i < this.list.Count; ++i)
            {
                if (this.getKeyFunction(this.list[i]).Equals(key))
                {
                    return i;
                }
            }

            return -1;
        }

        /// <summary>
        /// Tries to the get the item with the speficied key.
        /// </summary>
        /// <param name="key">The key of the item.</param>
        /// <param name="value">The value.</param>
        /// <returns>
        /// True if the item exists, false otherwise.
        /// </returns>
        public bool TryGetValue(KeyT key, out ItemT value)
        {
            return this.dictionary.TryGetValue(key, out value);
        }

        /// <summary>
        /// Removes the item with the specified key.
        /// </summary>
        /// <param name="key">The key of the item.</param>
        /// <returns>
        /// True if an item was removed, false otherwise.
        /// </returns>
        public bool Remove(KeyT key)
        {
            bool found = false;

            this.dictionary.Remove(key);
            for (int i = 0; i < this.list.Count; ++i)
            {
                if (this.getKeyFunction(this.list[i]).Equals(key))
                {
                    found = true;
                    this.list.RemoveAt(i);
                    break;
                }
            }

            return found;
        }

        /// <summary>
        /// Returns an enumerator that iterates through the collection.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.Collections.Generic.IEnumerator`1"/> that can be used to iterate through the collection.
        /// </returns>
        public IEnumerator<ItemT> GetEnumerator()
        {
            foreach (ItemT item in this.list)
            {
                yield return item;
            }
        }

        /// <summary>
        /// Clones this instance.
        /// </summary>
        /// <returns>The clone of this instance.</returns>
        public virtual object Clone()
        {
            ObservableKeyedCollection<KeyT, ItemT> c = new ObservableKeyedCollection<KeyT, ItemT>(this.getKeyFunction);
            foreach (ItemT item in this)
            {
                c.Add(item);
            }

            return c;
        }

        /// <summary>
        /// Determines the index of a specific item in the <see cref="T:System.Collections.Generic.IList`1"/>.
        /// </summary>
        /// <param name="item">The object to locate in the <see cref="T:System.Collections.Generic.IList`1"/>.</param>
        /// <returns>
        /// The index of <paramref name="item"/> if found in the list; otherwise, -1.
        /// </returns>
        public int IndexOf(ItemT item)
        {
            return this.list.IndexOf(item);
        }

        /// <summary>
        /// Determines whether the <see cref="T:System.Collections.Generic.ICollection`1"/> contains a specific value.
        /// </summary>
        /// <param name="item">The object to locate in the <see cref="T:System.Collections.Generic.ICollection`1"/>.</param>
        /// <returns>
        /// true if <paramref name="item"/> is found in the <see cref="T:System.Collections.Generic.ICollection`1"/>; otherwise, false.
        /// </returns>
        public bool Contains(ItemT item)
        {
            return this.Contains(this.getKeyFunction(item));
        }

        /// <summary>
        /// Copies the elements of the <see cref="T:System.Collections.Generic.ICollection`1"/> to an <see cref="T:System.Array"/>, starting at a particular <see cref="T:System.Array"/> index.
        /// </summary>
        /// <param name="array">The one-dimensional <see cref="T:System.Array"/> that is the destination of the elements copied from <see cref="T:System.Collections.Generic.ICollection`1"/>. The <see cref="T:System.Array"/> must have zero-based indexing.</param>
        /// <param name="arrayIndex">The zero-based index in <paramref name="array"/> at which copying begins.</param>
        /// <exception cref="T:System.ArgumentNullException">
        /// <paramref name="array"/> is null.</exception>
        /// <exception cref="T:System.ArgumentOutOfRangeException">
        /// <paramref name="arrayIndex"/> is less than 0.</exception>
        /// <exception cref="T:System.ArgumentException">
        /// <paramref name="array"/> is multidimensional.-or-The number of elements in the source <see cref="T:System.Collections.Generic.ICollection`1"/> is greater than the available space from <paramref name="arrayIndex"/> to the end of the destination <paramref name="array"/>.-or-Type <paramref name="T"/> cannot be cast automatically to the type of the destination <paramref name="array"/>.</exception>
        public void CopyTo(ItemT[] array, int arrayIndex)
        {
            this.list.CopyTo(array, arrayIndex);
        }

        /// <summary>
        /// Removes the first occurrence of a specific object from the <see cref="T:System.Collections.Generic.ICollection`1"/>.
        /// </summary>
        /// <param name="item">The object to remove from the <see cref="T:System.Collections.Generic.ICollection`1"/>.</param>
        /// <returns>
        /// true if <paramref name="item"/> was successfully removed from the <see cref="T:System.Collections.Generic.ICollection`1"/>; otherwise, false. This method also returns false if <paramref name="item"/> is not found in the original <see cref="T:System.Collections.Generic.ICollection`1"/>.
        /// </returns>
        /// <exception cref="T:System.NotSupportedException">The <see cref="T:System.Collections.Generic.ICollection`1"/> is read-only.</exception>
        public bool Remove(ItemT item)
        {
            return this.Remove(this.getKeyFunction(item));
        }

        /// <summary>
        /// Returns an enumerator that iterates through a collection.
        /// </summary>
        /// <returns>
        /// An <see cref="T:System.Collections.IEnumerator"/> object that can be used to iterate through the collection.
        /// </returns>
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }

        /// <summary>
        /// Handles the CollectionChanged event of the List control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.Collections.Specialized.NotifyCollectionChangedEventArgs"/> instance containing the event data.</param>
        private void List_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            var handler = this.CollectionChanged;
            if (handler != null)
            {
                handler(this, e);
            }
        }

        /// <summary>
        /// Gets the specified key.
        /// </summary>
        /// <param name="key">The key of the item.</param>
        /// <returns>The item with the speficied key.</returns>
        private ItemT Get(KeyT key)
        {
            return this.dictionary[key];
        }

        /// <summary>
        /// Sets an item with the specified key.
        /// </summary>
        /// <param name="key">The key of the item.</param>
        /// <param name="value">The value to set.</param>
        private void Set(KeyT key, ItemT value)
        {
            if (!this.getKeyFunction(value).Equals(key))
            {
                throw new Exception("The key of 'value' did not match the given key.");
            }

            if (this.dictionary.ContainsKey(key))
            {
                int foundIndex = -1;
                for (int i = 0; i < this.list.Count; i++)
                {
                    if (this.getKeyFunction(this.list[i]).Equals(key))
                    {
                        foundIndex = i;
                        break;
                    }
                }

                if (foundIndex == -1)
                {
                    throw new Exception("Identifier was found in the internal dictionary but not in the internal list.");
                }

                // Order here is important, as it is the list that sends the 
                this.dictionary[key] = value;
                this.list[foundIndex] = value;
            }
            else
            {
                this.Add(value);
            }
        }
    }
}
